<?php

class GXXML {

	// engine constants
	const GXXML_SIMPLEXML = 'SimpleXML';
	const GXXML_DOM = 'DOM';
	// if this is true, don't create a containing node for arrays and objects
	const GXXML_SKIP_CONTAINER = true;
	// if this is true, treat :method as a method call in __call()
	const GXXML_CALL = false;

	// gxxml config
	public $_GX;

	// load the driver
	public function __construct( $engine = null ){
		// store config as an object
		$this->_GX = new stdClass();
		$this->_GX->driver		= null;
		$this->_GX->root		= null;
		$this->_GX->doc			= null;
		$this->_GX->me			= null;
		$this->_GX->to			= null;
		$this->_GX->tag			= 'root';
		$this->_GX->version		= '1.0';
		$this->_GX->encoding	= 'UTF-8';
		$this->_GX->call		= array(
			'xpath',
			'XPATH',
			'XPath',
			'replaceNode',
			'setStyleSheet',
			'xml',
			'XML',
			'saveXML',
			'asXML'
			);
		return GXXMLTools::loadDriver( $this, $engine );
	}

	// return the appropriate xml engine
	public function __call( $method = null, $args = null ){
		return GXXMLTools::call( $this, $method, $args );
	}

}

class GXSimpleXML {

	public function __construct( &$doc = null, $tag = null, $value = null, $attributes = null, &$to = null, $options = null ){
		// it must be instantiating from GXXML
		if( !is_object($doc) && !$tag ){
			return $this;
		}
		// must have a tag
		if( !$tag || !is_string($tag) ){
			$tag = $this->_GX->tag;
		}

		// sense arguments
		$skipContainer = false;
		$args = func_get_args();
		for( $i = 0; $i < count($args); $i++ ){
			if( $args[$i] == GXDOM_SKIP_CONTAINER ){
				$skipContainer = true;
			}
		}

		// assign references
		$this->_GX->doc =& $doc;
		$this->_GX->to =& $to;
		if( is_object(GXXMLTools::getNode($this)) ){
			if( !is_object($this->_GX->to) ){
				$this->_GX->to =& GXXMLTools::getRootNode($this);
			}
		}

		// sanitize name
		$nodeName = GXXMLTools::cleanTag($tag);

		// if we have a value and it's an object or array, create a holding node, but only if we're not skipping it
		if( $value && (is_object($value) || is_array($value)) && !$skipContainer ){
			// attach this new node to "to", not to "me"
			$node = $this->$nodeName( null, null, $this->_GX->to );
			// assign the attach node to the new node
			$this->_GX->to =& $node->_GX->me;
		}
		if( !$node ){
			$node =& $this;
		}

		// handle different types of value
		switch( gettype($value) ){

			case 'object':
				// import the current element
				if( get_class($value) == 'GXSimpleXML' ){
					$in = $this->_GX->doc->importNode( $value->_GX->me, true );
				}
				if( $in ){
					$node->_GX->to->addChild($in);
					return $this;
				}
				// if it's not a GXSimpleXMLx object, convert it to an array and keep going
				$value = get_object_vars($value);
				//break; no break so goes directly to 'array'

			case 'array':
				// store the :to we were initially pointing to
				$holdTo =& $this->_GX->to;
				foreach( $value as $key => $val ){
					// if the key is numeric, use the parent's node name, since tags cannot be numbers
					unset($att);
					if( is_numeric($key) ){
						// add the number as @id
						$att = array('id'=>$key);
						$key = $this->_GX->to->getName();
						// if there's no root node, use the default tag name
						if( !$key ){
							$key = $this->_GX->tag;
						}
					}
					// use the node created above
					$node->$key( $val, $att, $this->_GX->to );
					// return the enclosing node
					$this->_GX->me =& $node->_GX->to;
				}
				// restore the original :to
				$this->_GX->me =& $holdTo;
				break;

			default:
				$cleanVal = GXXMLTools::cleanText(strval($value));
				if( !$this->_GX->doc ){
					if( !$cleanVal ){
						$str = "<$nodeName/>";
					}else{
						$str = "<$nodeName>$cleanVal</$nodeName>";
					}
					$this->_GX->doc = new SimpleXMLElement( $str );
					$SimpleNode = $this->_GX->doc;
				}else{
					$SimpleNode = $this->_GX->to->addChild( $nodeName, $cleanVal );
				}
				// attach attributes if we have them
				if( is_array($attributes) ){
					foreach( $attributes as $key => $val ){
						$SimpleNode->addAttribute( GXXMLTools::cleanTag($key), GXXMLTools::cleanText($val) );
					}
				}
				// assign latest reference
				$this->_GX->me =& $SimpleNode;

		}
		// return this GXSimpleXML
		return $this;
	}

	// see GXXMLTools::call for documentation
	public function __call( $method = null, $args = null ){
		return GXXMLTools::call( $this, $method, $args );
	}

}

class GXXMLTools {

	static function getMethods(){
		return array(
			'xpath',
			'XPATH',
			'XPath',
			'replaceNode',
			'setStyleSheet',
			'xml',
			'XML',
			'saveXML',
			'asXML'
			);
	}

	/*
	 * loadDriver
	 *
	 */
	static function loadDriver( &$obj, $engine = null ){
		// only valid values are SIMPLE_XML and DOM, defaulting to SIMPLE_XML
		switch( $engine ){
			case DOM:
				$obj->_GX->driver = 'GXDOM';
				break;
			case SIMPLE_XML:
			default:
				$obj->_GX->driver = 'GXSimpleXML';
		}
		// instantiate the driver
		$obj->_GX->root =& new $obj->_GX->driver();
		return $this;
	}

	static function getNode( &$obj, $type = 'doc' ){
		switch( $type ){
			case 'me':
				// if there's no me, return the doc
				return ($obj->_GX->me) ? $obj->_GX->me : $obj->_GX->doc;
				break;
			case 'to':
				return $obj->_GX->to;
				/*
				if( $obj->_GX->to ){
					return $obj->_GX->to;
				}else{
					return $obj->_GX->doc;
				}
				*/
				break;
			case 'doc':
			default:
				return $obj->_GX->doc;
		}
	}

	static function getRootNode( &$obj = null ){
		if( strpos(get_class($obj),'Simple') || is_numeric(strpos($obj->_GX->driver,'Simple')) ){
			// return the first child of the document
			$node = GXXMLTools::getNode($obj);
			if( is_object($node) ){
				$c = $node->children();
				return $c[0];
			}
			return false;
		}else{
			// return the document element or just the doc itself
			$elm = ($obj->_GX->doc->documentElement) ? $obj->_GX->doc->documentElement : $obj->_GX->doc;
			return $elm;
		}
		/*
		// check for a root node
		if( $tmp = GXXMLTools::xpath( GXXMLTools::getNode($this), '/*[1]' ) ){
			$this->_GX->to =& $tmp[0];
		}else{
			$this->_GX->to =& $this->_GX->doc;
		}
		*/
	}

	/*
	 * call
	 *
	 * Called by GXSimpleXML::__call or GXDOM::__call, which treat the value of @method as a tag name.
	 *
	 * @param String the name of the method called
	 * @param Array arguments passed to this method
	 * @returns Mixed GXSimpleXMLElement on success, false on failure
	 */
	static function call( &$obj, $method = null, $args = null ){
		if( $method != get_class($obj) ){
			$tag = $method;
			$value = $args[0];
			$attributes = $args[1];
			$to = $args[2];
			if( !is_object($to) ){
				$to = GXXMLTools::getNode($obj,'me');
			}

			for( $i = 0; $i < count($args); $i++ ){
				if( $args[$i] == GXXML_CALL ){
					$callMethod = true;
				}
			}

			// if the method is one of the reserved function names AND we're NOT forcing it to be used as one...
			if( in_array($method,GXXMLTools::getMethods()) && $callMethod ){
				// handle reserved method names
				// can't use these in your xml file
				switch( $method ){
					case 'xpath':
					case 'XPATH':
					case 'XPath':
						return GXXMLTools::xpath( GXXMLTools::getNode($obj,'me'), $args[0] );
						break;
					case 'replaceNode':
						// @doc, @replaceWith, @toReplace, @xpath
						return GXXMLTools::replaceNode( GXXMLTools::getNode($obj,'me'), $args[0], $args[1], $args[2] );
						break;
					case 'setStyleSheet':
						// @doc, @href
						return GXXMLTools::setStyleSheet( GXXMLTools::getNode($obj,'doc'), $args[0] );
						break;
					case 'xml':
					case 'XML':
					case 'saveXML':
					case 'asXML':
						// @doc
						return GXXMLTools::xml( GXXMLTools::getNode($obj,'me') );
						break;
				}
			}
			// if we're here, we didn't call a method, so create a new node
			if( $obj->_GX->driver == 'GXSimpleXML' ){
				return new GXSimpleXML( GXXMLTools::getNode($obj), $tag, $value, $attributes, $to );
			}else{
				return new GXDOM( GXXMLTools::getNode($obj), $tag, $value, $attributes, $to );
			}
		}
		trigger_error( 'Invalid method call', E_USER_ERROR );
		return false;
	}

	static function xpath( &$node = null, $xpath = null ){
		if( !is_object($node) || !$xpath ){
			trigger_error( 'Missing information: node and xpath must both be present', E_USER_WARNING );
			return false;
		}
		// grab the engine
		$engine = ( strpos(get_class($obj),'Simple') ) ? 'SimpleXML' : 'DOM' ;
		// execute the xpath
		if( $engine == 'SimpleXML' ){
			$xresult = $node->xpath($xpath);
		}else{
			$x = new DOMXPath( $node->_GX->doc );
			$context = ($node->_GX->me) ? $node->_GX->me : $node->_GX->doc;
			$xresult = $x->query( $xpath, $context );
		}
		// verify it was successful
		if( !$xresult ){
			// we got a false result, which is an error
			if( gettype($xresult) == 'boolean' ){
				trigger_error( 'Unable to execute xpath query: ' .$xpath, E_USER_WARNING );
				return false;
			}
		}
		// return the results
		return $xresult;
	}

	/*
	 * replaceNode
	 *
	 * Imports another doc, then replaces the specified node in this doc with it.
	 *
	 * @param Object the new TDOM object
	 * @param Object the node to be replaced
	 * @param String an xpath to be run on this document which identifies the node to be replaced
	 * @return Boolean
	 */
	static function replaceNode( &$doc, $replaceWith = null, $node = null, $xpath = null ){
		// Must have an object for replaceWith and either node or xpath
		if( !is_object($doc) || !is_object($replaceWith) || !($node || $xpath) ){
			trigger_error( 'Missing information: doc and replaceWith must be objects and either node or xpath is required', E_USER_ERROR );
			return false;
		}
		// If we have an xpath, set Node to the query results
		if( $xpath ){
			$tmp = $this->get( $xpath, null, 'dom' );
			$node = $tmp->item(0);
		}
		// Now perform the replacement
		$tmp = $doc->importNode( $replaceWith->doc->documentElement, true );
		if( !$node->parentNode ){
			trigger_error( 'parentNode of this node is invalid or missing, appending to this doc', E_USER_WARNING );
			$doc->documentElement->appendChild( $tmp );
		}else{
			$node->parentNode->replaceChild( $tmp, $node );
		}
		return true;
	}

	/*
	 * setStyleSheet
	 *
	 * Adds the xml-stylesheet processing instruction at the top of this document.
	 *
	 * @param String relative path to the XSL file
	 * @return boolean
	 */
	static function setStyleSheet( &$doc, $path = null ){
		if( !$path ){
			trigger_error( 'Missing information: path must be a string', E_USER_ERROR );
			return false;
		}
		$instr = $doc->createProcessingInstruction( 'xml-stylesheet', 'href="' .$path .'" type="text/xsl"' );
		$doc->insertBefore( $instr, $doc->documentElement );
		return true;
	}

	/*
     * xml
     *
     * Generates xml string
     *
     * @return string
     */
	static function xml( &$doc ){
		if( strpos(get_class($doc),'Simple') ){
			return $doc->asXML();
		}else{
			return $doc->saveXML();
		}
		trigger_error( 'Can\'t generate xml from provided object: ' .get_class($doc), E_USER_ERROR );
		return false;
	}

	/*
	 * tag
	 *
	 * Returns a string valid for use as a node name or attribute name.
	 *
	 * @param String value to sanitize
	 * @return String new name
	 */
	static function cleanTag( $name = null ){
		if( !$name ){
			return false;
		}
		// If it's not a valid node name, sanitize it.
		$from = array('/','%','$','(',')','{','}','#');
		$tmp = trim( str_replace($from,'_',$name) );
		// If trimming it results in nothing, call it entry.
		if( !$tmp ){
			$tmp = 'entry';
		}
		// Names must begin with a letter. If it doesn't, prepend it with an underscore.
		if( preg_match( "/[^a-z]+/i", substr($tmp,0,1) ) && substr($tmp,0,1) != '_' ){
			debug( 'Key name (' .$name .') is not valid, returning _' .$tmp .' / ' .E_USER_ERROR );
			$tmp = '_' .$tmp;
		}
		return $tmp;
	}

	/*
	 * escapeText
	 *
	 * Returns values suitable for inclusion in a UTF-8 document.
	 * If the document will not be UTF-8, it just returns the value, untouched.
	 *
	 * @param String value to be escaped
	 * @return escaped value
	 */
	static function cleanText( $str = null ){
		if( !strlen($str) ){
			return false;
		}
		// Assume we're encoding to UTF-8, so escape MS Windows-1252 characters, then run iconv on it.
		// Note: your editor must be set to ISO-8859-1 or UTF-8, or the $map array below will no longer work.
		// You should see actual characters in every other key below.
		$map = array(
			"&"     => '&amp;',     // 2 'must be' escaped chars in xml
			"<"     => '&lt;',
			"•"     => '&#x2022;',  // bullet: ascii 149
			"\x95"  => '&#x2022;',
			" "     => '&#160;',    // non-breaking space: ascii 160
			"\xA0"  => '&#160;',
			"©"     => '&#169;',    // copyright: ascii 169
			"\xA9"  => '&#169;',
			"®"     => '&#174;',    // registered: ascii 174
			"\xAE"  => '&#174;',
			"™"     => '&#x2122;',	// trademark: ascii 153
			"\x99"  => '&#x2122;',
			"…"     => '&#x2026;',	// ellipsis: ascii 133
			"\x85"  => '&#x2026;',
			"‘"     => '&#x2018;',	// opening single quote: ascii 145
			"\x91"  => '&#x2018;',
			"’"     => '&#x2019;',	// closing single quote: ascii 146
			"\x92"  => '&#x2019;',
			"“"     => '&#x201C;',	// opening double quote: ascii 147
			"\x93"  => '&#x201C;',
			"”"     => '&#x201D;',	// closing double quote: ascii 148
			"\x94"  => '&#x201D;',
			"–"     => '&#x2013;',	// en dash: ascii 150
			"\x96"  => '&#x2013;',
			"—"     => '&#x2014;',	// em dash: ascii 151
			"\x97"  => '&#x2014;'
			);
		// First, run the translation for map.
		$temp = strtr( $str, $map );
		// Finally, run iconv to take care of any remaining encoding issues.
		return iconv( 'ISO-8859-1', 'UTF-8//TRANSLIT', $temp );
	}

}











class GXDOM extends GXXML {

	public function __construct( &$doc, $tag = null, $value = null, $attributes = null, &$to = null, $options = null ){
		// must have a tag
		if( !$tag || !is_string($tag) ){
			$tag = $this->defaultTag;
		}

		// sense arguments
		$skipContainer = false;
		$args = func_get_args();
		for( $i = 0; $i < count($args); $i++ ){
			if( $args[$i] == GXXML_SKIP_CONTAINER ){
				$skipContainer = true;
			}
		}

		// assign references
		$this->_GXdoc = $doc;
		$this->_GX->to = $to;
		if( !is_object($this->_GX->to) ){
			$this->_GX->to = ($this->_GX->doc->documentElement) ? $this->_GX->doc->documentElement : $this->_GX->doc;
		}

		// sanitize name
		$nodeName = GXXMLTools::cleanTag($tag);

		// if we have a value and it's an object or array, create a holding node, but only if we're not skipping it
		if( $value && (is_object($value) || is_array($value)) && !$skipContainer ){
			// attach this new node to "to", not to "me"
			$node = $this->$nodeName(null,null,$this->_GX->to);
			// assign the attach node to the new node
			$this->_GX->to = $node->_GX->me;
		}
		if( !$node ){
			$node = $this;
		}

		// handle different types of value
		switch( gettype($value) ){

			case 'object':
				// import the current element
				if( get_class($value) == 'GXDOM' ){
					$in = $this->_GX->doc->importNode( $value->_GX->me, true );
				}
				if( $in ){
					$node->_GX->to->appendChild($in);
					return $this;
				}
				// if it's not a GXDOMx object, convert it to an array and keep going
				$value = get_object_vars($value);
				//break; no break so goes directly to 'array'

			case 'array':
				// store the :to we were initially pointing to
				$holdTo = $this->_GX->to;
				foreach( $value as $key => $val ){
					// if the key is numeric, use the parent's node name, since tags cannot be numbers
					unset($att);
					if( is_numeric($key) ){
						// add the number as @id
						$att = array('id'=>$key);
						$key = $this->_GX->to->nodeName;
						// if there's no root node, use the default tag name
						if( $key == '#document' ){
							$key = $this->defaultTag;
						}
					}
					// use the node created above
					$node->$key($val,$att,$this->_GX->to);
					// return the enclosing node
					$this->_GX->me = $node->_GX->to;
				}
				// restore the original :to
				$this->_GX->me = $holdTo;
				break;

			default:
				// if it has reserved characters, try to load it as an xml fragment
				// if it won't parse, make it a CDATA section
				if( preg_match("/[<>&]/", $value) ){
					$frag = $this->_GX->doc->createDocumentFragment();
					$DOMnode = $this->_GX->doc->createElement($nodeName);
					// suppress error
					if( @$frag->appendXML($value) ){
						$DOMnode->appendChild( $frag );
					}else{
						$DOMnode->appendChild( $this->_GX->doc->createCDATASection(strval($value)) );
					}
				}else{
					$DOMnode = $this->_GX->doc->createElement( $nodeName, GXXMLTools::cleanText( strval($value) ) );
				}
				// attach attributes if we have them
				if( is_array($attributes) ){
					foreach( $attributes as $key => $val ){
						$DOMnode->setAttribute( GXXMLTools::cleanTag($key), GXXMLTools::cleanText($val) );
					}
				}
				$this->_GX->to->appendChild($DOMnode);
				// assign latest reference
				$this->_GX->me = $DOMnode;

		}
		// return this GXDOMElement
		return $this;
	}

	/*
	 * __call
	 *
	 * Treats the value of @method as a tag name.
	 *
	 * @param String the name of the method called
	 * @param Array arguments passed to this method
	 * @returns Mixed GXDOMElement on success, false on failure
	 */
	public function __call( $method, $args ){
		if( $method != __CLASS__ ){
			$tag = $method;
			$value = $args[0];
			$attributes = $args[1];
			$to = $args[2];
			if( !is_object($to) ){
				$to = $this->_GX->me;
			}
			for( $i = 0; $i < count($args); $i++ ){
				if( $args[$i] == CALL ){
					$callMethod = true;
				}
			}

			$validMethods = array(
				'xpath',
				'XPath',
				'replaceNode',
				'setStyleSheet',
				'xml',
				'XML',
				'saveXML',
				'asXML'
				);
			// if the method is one of the reserved function names AND we're NOT forcing it to be used as one...
			if( in_array($method,$validMethods) && $callMethod ){
				// handle reserved method names
				// can't use these in your xml file
				switch( $method ){
					case 'xpath':
					case 'XPATH':
					case 'XPath':
						return GXXMLTools::xpath( $this, $args[0] );
						break;
					case 'replaceNode':
						// @doc, @replaceWith, @toReplace, @xpath
						return GXXMLTools::replaceNode( $this->_GXdoc, $args[0], $args[1], $args[2] );
						break;
					case 'setStyleSheet':
						// @doc, @href
						return GXXMLTools::setStyleSheet( $this->_GXdoc, $args[0] );
						break;
					case 'xml':
					case 'XML':
					case 'saveXML':
					case 'asXML':
						// @doc
						return GXXMLTools::xml( $this->_GXdoc );
						break;
					default:
						return new GXDOMElement( $this->_GXdoc, $tag, $value, $attributes, $to );
				}
			}else{
				return new GXDOMElement( $this->_GXdoc, $tag, $value, $attributes, $to );
			}
		}
		trigger_error( 'Invalid method call in GXDOM', E_USER_ERROR );
		return false;
	}

}

?>